﻿using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using CkSoftware.GroupMe.Sdk.Core.Dao;
using CkSoftware.GroupMe.Sdk.Core.Exceptions;
using CkSoftware.GroupMe.Sdk.Core.Interfaces;
using Newtonsoft.Json;

namespace CkSoftware.GroupMe.Sdk.Core
{
	/// <summary>
	/// Basic WebApi Access Class.
	/// </summary>
	public class WebApiAccess : IWebApiAccess
	{
		private readonly string _accessToken;

		/// <summary>
		/// The Base-URL for the WebApi / REST Service.
		/// </summary>
		private readonly string _baseUrl;

		/// <summary>
		/// The HTTP Client used for communitcation with the server.
		/// </summary>
		private readonly HttpClient _httpClient;

		private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings
		{
			NullValueHandling = NullValueHandling.Ignore
		};

		/// <summary>
		/// Instantiates a new WebApiAccess instance for the given base-url.
		/// </summary>
		/// <param name="baseUrl">
		/// The base-url for the WebApi / REST Service.
		/// <remarks>
		/// Will be concatenated with the command name (base-url "http://tempuri.org/api" and command name "members" will
		/// result in "http://tempuri.org/api/members").
		/// </remarks>
		/// </param>
		/// <param name="accessToken">The Access Token which will be added for each request.</param>
		public WebApiAccess(string baseUrl, string accessToken)
		{
			_baseUrl = baseUrl;
			_accessToken = accessToken;
			AccessTokenParameter = "token";

			_httpClient = new HttpClient();
		}

		/// <summary>
		/// Executes the given command asynchronousely and returns an awaitable task containing the expected result.
		/// </summary>
		/// <typeparam name="TResultEntity">Result entity which will be deserialized from the WebApi / REST service.</typeparam>
		/// <param name="command">The command to execute.</param>
		/// <returns>Awaitable task returning the given result entity.</returns>
		public async Task<TResultEntity> ExecuteCommand<TResultEntity>(IWebApiCommand<TResultEntity> command)
			where TResultEntity : IWebApiResult, new()
		{
			Trace.WriteLine("Starting command execution for command '" + PrepareContentForTrace(command.CommandName) + "'");

			ApiResponseWithStatusCode commandResult =
				await GetJsonResultFromCommand(command.CommandName, command.Parameters, command.HttpMethod);
			command.CommandExecuted = true;

			if (string.IsNullOrEmpty(commandResult.ApiResponse))
			{
				return new TResultEntity {Meta = new MetaData {Code = (int) commandResult.StatusCode}};
			}

			return await ParseJsonResultToEntity<TResultEntity>(commandResult.ApiResponse);
		}

		/// <summary>
		/// The name of the access-token url-parameter.
		/// </summary>
		public string AccessTokenParameter { get; set; }

		/// <summary>
		/// Generates a query-string for the given parameters public properties, respecting JsonProperty attributes.
		/// </summary>
		/// <param name="requestUri">The Uri which should be enhanced.</param>
		/// <param name="parameters">The parameters to create the query-string from.</param>
		/// <returns>The generated query-string for the parameter.</returns>
		private Uri AddQueryStringToUriForParameters(Uri requestUri, IWebApiCommandParameters parameters)
		{
			NameValueCollection query = HttpUtility.ParseQueryString(requestUri.Query);

			if (parameters != null)
			{
				Type baseType = parameters.GetType().DeclaringType ?? parameters.GetType();

				foreach (PropertyInfo publicProperty in baseType.GetProperties())
				{
					object value = publicProperty.GetValue(parameters);
					if (value != null)
					{
						string name = GetNameForPropertyIncludingJsonFieldDefinition(publicProperty);
						query.Add(name, value.ToString());
					}
				}
			}

			var uriBuilder = new UriBuilder(requestUri) {Query = query.ToString()};

			return uriBuilder.Uri;
		}

		private HttpContent CreateRequestContentFromParameters(IWebApiCommandParameters parameters)
		{
			var fileUploadWebApiCommandParameter = parameters as FileUploadWebApiCommandParameter;
			if (fileUploadWebApiCommandParameter != null)
			{
				string boundaryString = string.Concat("-------------", Guid.NewGuid().ToString("N"));
				var requestContent = new MultipartFormDataContent(boundaryString);

				var fileData = new StreamContent(fileUploadWebApiCommandParameter.FileData);
				requestContent.Add(fileData, fileUploadWebApiCommandParameter.Name, fileUploadWebApiCommandParameter.FileName);

				requestContent.Headers.ContentType.MediaType = "multipart/form-data";
				requestContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", boundaryString));

				return requestContent;
			}

			string content = SerializeParameters(parameters);
			Trace.WriteLine("Executing WebApi call with content '" + PrepareContentForTrace(content) + "'");
			return new StringContent(content, Encoding.UTF8, "application/json");
		}

		/// <summary>
		/// Creates the request url for the given command name.
		/// </summary>
		/// <param name="commandName">The command name to be used, must not contain any non alphanumeric characters.</param>
		/// <returns>The usable request url for the current WebApiAccess with the given command name.</returns>
		private Uri CreateRequestUrlForCommand(string commandName)
		{
			string cleanedBaseUrl = _baseUrl.EndsWith("/", StringComparison.CurrentCultureIgnoreCase)
				? _baseUrl.Substring(0, _baseUrl.Length - 1)
				: _baseUrl;

			NameValueCollection query =
				HttpUtility.ParseQueryString(string.Format(CultureInfo.CurrentCulture,
					"?{0}={1}",
					AccessTokenParameter,
					_accessToken));
			var uriBuilder = new UriBuilder(string.Concat(cleanedBaseUrl, "/", commandName))
			{
				Port = -1,
				Query = query.ToString()
			};

			return uriBuilder.Uri;
		}

		private async Task<HttpResponseMessage> ExecuteRequestAndGetResponse(IWebApiCommandParameters parameters,
			HttpMethod httpMethod,
			Uri requestUri,
			HttpContent requestContent)
		{
			if (httpMethod == HttpMethod.Post)
			{
				Trace.WriteLine("Posting to WebApi URL '" + PrepareContentForTrace(requestUri.ToString()) + "'");
				return await _httpClient.PostAsync(requestUri, requestContent);
			}

			if (httpMethod == HttpMethod.Put)
			{
				Trace.WriteLine("Putting to WebApi URL '" + PrepareContentForTrace(requestUri.ToString()) + "'");
				return await _httpClient.PutAsync(requestUri, requestContent);
			}

			if (httpMethod == HttpMethod.Get)
			{
				Uri uri = AddQueryStringToUriForParameters(requestUri, parameters);

				Trace.WriteLine("Getting to WebApi URL '" + PrepareContentForTrace(uri.ToString()) + "'");

				return await _httpClient.GetAsync(uri);
			}

			throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture,
				"The HttpMethod '{0}' is not supported.",
				httpMethod));
		}

		private string PrepareContentForTrace(string content)
		{
			string result = content.Replace(_accessToken, "ACCESS-TOKEN");
			return result;
		}

		/// <summary>
		/// Asynchronousely executes the given command name with the given parameters using the given http method.
		/// </summary>
		/// <param name="commandName">The command name which should be executed.</param>
		/// <param name="parameters">The parameters object containing the parameters which should be attached to the request.</param>
		/// <param name="httpMethod">
		/// The http method which should be executed.
		/// <remarks>Only POST, PUT and GET are currently supported.</remarks>
		/// </param>
		/// <returns>Awaitable task returning the response body as string.</returns>
		private async Task<ApiResponseWithStatusCode> GetJsonResultFromCommand(string commandName,
			IWebApiCommandParameters parameters,
			HttpMethod httpMethod)
		{
			Uri requestUri = CreateRequestUrlForCommand(commandName);
			HttpContent requestContent = CreateRequestContentFromParameters(parameters);
			HttpResponseMessage response = await ExecuteRequestAndGetResponse(parameters, httpMethod, requestUri, requestContent);

			var result = new ApiResponseWithStatusCode
			{
				StatusCode = response.StatusCode,
				ApiResponse = await response.Content.ReadAsStringAsync()
			};

			return result;
		}

		/// <summary>
		/// Reads the JsonPropertyAttribute out of the given PropertyInfo object or returns just the name of the property.
		/// </summary>
		/// <param name="publicProperty">The PropertyInfo to read the name from.</param>
		/// <returns>The defined JsonPropertyAttribute.PropertyName or just the name of the property.</returns>
		private string GetNameForPropertyIncludingJsonFieldDefinition(PropertyInfo publicProperty)
		{
			var jsonPropertyAttribute = publicProperty.GetCustomAttribute<JsonPropertyAttribute>();
			if (jsonPropertyAttribute != null && !string.IsNullOrEmpty(jsonPropertyAttribute.PropertyName))
			{
				return jsonPropertyAttribute.PropertyName;
			}

			return publicProperty.Name;
		}

		/// <summary>
		/// Asynchronousely parses the result into the given typed object.
		/// </summary>
		/// <typeparam name="TEntityType">The type which the data should be deserialized to.</typeparam>
		/// <param name="jsonResult">The json result containing the data to deserialize.</param>
		/// <returns>The deserialized data.</returns>
		private async Task<TEntityType> ParseJsonResultToEntity<TEntityType>(string jsonResult)
		{
			Trace.WriteLine("Trying to parse result '" + PrepareContentForTrace(PrettyPrintJson(jsonResult)) + "'");

			try
			{
				return await JsonConvert.DeserializeObjectAsync<TEntityType>(jsonResult, _serializerSettings);
			}
			catch (JsonReaderException ex)
			{
				Trace.WriteLine(string.Format(CultureInfo.CurrentCulture,
					"JsonReaderException occured: {0}\r\nStack trace:\r\n{1}",
					ex.Message,
					ex.StackTrace));
				throw new GroupMeException(new MetaData
				{
					Code = 500,
					Errors = new[] {"Unexpected Json Deserialization error occured."}
				});
			}
		}

		private string PrettyPrintJson(string jsonInput)
		{
			if (Trace.Listeners.Count > 0)
			{
				dynamic parsed = JsonConvert.DeserializeObject(jsonInput);
				return JsonConvert.SerializeObject(parsed, Formatting.Indented, _serializerSettings);
			}

			return jsonInput;
		}

		/// <summary>
		/// Serializes the given parameters object using JsonConvert.
		/// </summary>
		/// <param name="parameters">The parameters to serialize.</param>
		/// <returns>The serialized representation of the parameters.</returns>
		private string SerializeParameters(IWebApiCommandParameters parameters)
		{
			if (parameters == null)
			{
				return string.Empty;
			}

			return JsonConvert.SerializeObject(parameters, Formatting.Indented, _serializerSettings);
		}
	}
}